diff options
Diffstat (limited to 'app/[lng]/admin/ecc/page.tsx')
| -rw-r--r-- | app/[lng]/admin/ecc/page.tsx | 706 |
1 files changed, 706 insertions, 0 deletions
diff --git a/app/[lng]/admin/ecc/page.tsx b/app/[lng]/admin/ecc/page.tsx new file mode 100644 index 00000000..bb3168c4 --- /dev/null +++ b/app/[lng]/admin/ecc/page.tsx @@ -0,0 +1,706 @@ +'use client' + +import { useState } from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' + +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Loader2, Play, CheckCircle, XCircle, Send } from 'lucide-react' +import { toast } from 'sonner' + +// SOAP 송신 함수들 import +import { confirmTestPCR, confirmPCR } from '@/lib/soap/ecc/send/pcr-confirm' +import { cancelTestRFQ, cancelRFQ } from '@/lib/soap/ecc/send/cancel-rfq' +import { sendTestRFQInformation, sendRFQInformation } from '@/lib/soap/ecc/send/rfq-info' +import { createTestPurchaseOrder, createPurchaseOrder } from '@/lib/soap/ecc/send/create-po' + +interface TestResult { + success: boolean + message: string + responseData?: string + timestamp: string + duration?: number +} + +export default function ECCSenderTestPage() { + const [isLoading, setIsLoading] = useState<{ [key: string]: boolean }>({}) + const [testResults, setTestResults] = useState<{ [key: string]: TestResult }>({}) + + // 테스트 실행 공통 함수 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const runTest = async (testName: string, testFunction: () => Promise<any>) => { + setIsLoading(prev => ({ ...prev, [testName]: true })) + const startTime = Date.now() + + try { + const result = await testFunction() + const duration = Date.now() - startTime + + const testResult: TestResult = { + success: result.success, + message: result.message, + responseData: result.responseData, + timestamp: new Date().toLocaleString('ko-KR'), + duration + } + + setTestResults(prev => ({ ...prev, [testName]: testResult })) + + if (result.success) { + toast.success(`${testName} 테스트 성공`) + } else { + toast.error(`${testName} 테스트 실패: ${result.message}`) + } + } catch (error) { + const testResult: TestResult = { + success: false, + message: error instanceof Error ? error.message : '알 수 없는 오류', + timestamp: new Date().toLocaleString('ko-KR'), + duration: Date.now() - startTime + } + + setTestResults(prev => ({ ...prev, [testName]: testResult })) + toast.error(`${testName} 테스트 오류: ${testResult.message}`) + } finally { + setIsLoading(prev => ({ ...prev, [testName]: false })) + } + } + + // PCR 확인 테스트 + const [pcrData, setPcrData] = useState({ + PCR_REQ: 'TEST_PCR01', + PCR_REQ_SEQ: '00001', + PCR_DEC_DATE: '20241201', + EBELN: 'TEST_PO01', + EBELP: '00010', + PCR_STATUS: 'A', + WAERS: 'KRW', + PCR_NETPR: '1000.00', + PEINH: '1', + PCR_NETWR: '1000.00', + CONFIRM_CD: 'CONF', + CONFIRM_RSN: '테스트 확인' + }) + + // RFQ 취소 테스트 + const [rfqCancelData, setRfqCancelData] = useState({ + ANFNR: 'TEST_RFQ_001' + }) + + // RFQ 정보 전송 테스트 + const [rfqInfoData, setRfqInfoData] = useState({ + // 헤더 정보 + ANFNR: 'RFQ0000001', + LIFNR: '1000000001', + WAERS: 'KRW', + ZTERM: '0001', + INCO1: 'FOB', + INCO2: 'Seoul, Korea', + MWSKZ: 'V0', + LANDS: 'KR', + VSTEL: '001', + LSTEL: '001', + // 아이템 정보 + ANFPS: '00001', + NETPR: '1000.00', + NETWR: '1000.00', + BRTWR: '1100.00', + LFDAT: '20241201' + }) + + // PO 생성 테스트 + const [poData, setPoData] = useState({ + // 헤더 정보 + ANFNR: 'TEST001', + LIFNR: '1000000001', + ZPROC_IND: 'A', + ANGNR: 'TEST001', + WAERS: 'KRW', + ZTERM: '0001', + INCO1: 'FOB', + INCO2: 'Seoul, Korea', + MWSKZ: 'V0', + LANDS: 'KR', + ZRCV_DT: '20241201', + ZATTEN_IND: 'Y', + IHRAN: '20241201', + TEXT: 'Test PO Creation', + // 아이템 정보 + ANFPS: '00001', + NETPR: '1000.00', + PEINH: '1', + BPRME: 'EA', + NETWR: '1000.00', + BRTWR: '1100.00', + LFDAT: '20241201', + // PR 반환 정보 + EBELN: 'PR001', + EBELP: '00001', + MSGTY: 'S', + MSGTXT: 'Test message' + }) + + return ( + <div className="container mx-auto py-8 space-y-6"> + <div className="flex items-center justify-between"> + <div> + <h1 className="text-3xl font-bold">ECC SOAP Sender 테스트</h1> + <p className="text-muted-foreground mt-2"> + 4개의 ECC SOAP 송신 라이브러리를 테스트합니다 + </p> + </div> + <Badge variant="outline" className="text-sm"> + 개발/테스트 환경 + </Badge> + </div> + + <Tabs defaultValue="pcr-confirm" className="w-full"> + <TabsList className="grid w-full grid-cols-4"> + <TabsTrigger value="pcr-confirm">PCR 확인</TabsTrigger> + <TabsTrigger value="rfq-cancel">RFQ 취소</TabsTrigger> + <TabsTrigger value="rfq-info">RFQ 정보</TabsTrigger> + <TabsTrigger value="po-create">PO 생성</TabsTrigger> + </TabsList> + + {/* PCR 확인 탭 */} + <TabsContent value="pcr-confirm" className="space-y-6"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Send className="h-5 w-5" /> + PCR (Price Change Request) 확인 + </CardTitle> + <CardDescription> + PCR 확인 요청을 ECC로 전송합니다. (IF_ECC_EVCP_PCR_CONFIRM) + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Label htmlFor="pcr-req">PCR 요청번호 (필수)</Label> + <Input + id="pcr-req" + value={pcrData.PCR_REQ} + onChange={(e) => setPcrData(prev => ({ ...prev, PCR_REQ: e.target.value }))} + placeholder="PCR 요청번호 (최대 10자)" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="pcr-seq">PCR 요청순번 (필수)</Label> + <Input + id="pcr-seq" + value={pcrData.PCR_REQ_SEQ} + onChange={(e) => setPcrData(prev => ({ ...prev, PCR_REQ_SEQ: e.target.value }))} + placeholder="PCR 요청순번 (5자리 숫자)" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="pcr-date">PCR 결정일 (필수)</Label> + <Input + id="pcr-date" + value={pcrData.PCR_DEC_DATE} + onChange={(e) => setPcrData(prev => ({ ...prev, PCR_DEC_DATE: e.target.value }))} + placeholder="YYYYMMDD 형식" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="ebeln">구매오더 (필수)</Label> + <Input + id="ebeln" + value={pcrData.EBELN} + onChange={(e) => setPcrData(prev => ({ ...prev, EBELN: e.target.value }))} + placeholder="구매오더 번호" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="ebelp">구매오더 품번 (필수)</Label> + <Input + id="ebelp" + value={pcrData.EBELP} + onChange={(e) => setPcrData(prev => ({ ...prev, EBELP: e.target.value }))} + placeholder="구매오더 품번" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="pcr-status">PCR 상태 (필수)</Label> + <Input + id="pcr-status" + value={pcrData.PCR_STATUS} + onChange={(e) => setPcrData(prev => ({ ...prev, PCR_STATUS: e.target.value }))} + placeholder="PCR 상태 (1자)" + /> + </div> + </div> + + <Separator /> + + <div className="flex gap-4"> + <Button + onClick={() => runTest('PCR 확인 (샘플)', () => confirmTestPCR())} + disabled={isLoading['PCR 확인 (샘플)']} + variant="outline" + > + {isLoading['PCR 확인 (샘플)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Play className="mr-2 h-4 w-4" /> + 샘플 데이터로 테스트 + </Button> + <Button + onClick={() => runTest('PCR 확인 (사용자)', () => confirmPCR(pcrData))} + disabled={isLoading['PCR 확인 (사용자)']} + > + {isLoading['PCR 확인 (사용자)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Send className="mr-2 h-4 w-4" /> + 사용자 데이터로 전송 + </Button> + </div> + + {/* 테스트 결과 표시 */} + {(testResults['PCR 확인 (샘플)'] || testResults['PCR 확인 (사용자)']) && ( + <div className="space-y-2"> + <h4 className="font-semibold">테스트 결과</h4> + {testResults['PCR 확인 (샘플)'] && ( + <TestResultCard result={testResults['PCR 확인 (샘플)']} title="샘플 테스트" /> + )} + {testResults['PCR 확인 (사용자)'] && ( + <TestResultCard result={testResults['PCR 확인 (사용자)']} title="사용자 테스트" /> + )} + </div> + )} + </CardContent> + </Card> + </TabsContent> + + {/* RFQ 취소 탭 */} + <TabsContent value="rfq-cancel" className="space-y-6"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Send className="h-5 w-5" /> + RFQ (Request for Quotation) 취소 + </CardTitle> + <CardDescription> + RFQ 취소 요청을 ECC로 전송합니다. (IF_ECC_EVCP_CANCEL_RFQ) + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="anfnr-cancel">RFQ 번호 (필수)</Label> + <Input + id="anfnr-cancel" + value={rfqCancelData.ANFNR} + onChange={(e) => setRfqCancelData(prev => ({ ...prev, ANFNR: e.target.value }))} + placeholder="RFQ 번호 (최대 10자)" + /> + </div> + + <Separator /> + + <div className="flex gap-4"> + <Button + onClick={() => runTest('RFQ 취소 (샘플)', () => cancelTestRFQ())} + disabled={isLoading['RFQ 취소 (샘플)']} + variant="outline" + > + {isLoading['RFQ 취소 (샘플)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Play className="mr-2 h-4 w-4" /> + 샘플 데이터로 테스트 + </Button> + <Button + onClick={() => runTest('RFQ 취소 (사용자)', () => cancelRFQ(rfqCancelData.ANFNR))} + disabled={isLoading['RFQ 취소 (사용자)']} + variant="destructive" + > + {isLoading['RFQ 취소 (사용자)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Send className="mr-2 h-4 w-4" /> + 사용자 데이터로 취소 + </Button> + </div> + + {/* 테스트 결과 표시 */} + {(testResults['RFQ 취소 (샘플)'] || testResults['RFQ 취소 (사용자)']) && ( + <div className="space-y-2"> + <h4 className="font-semibold">테스트 결과</h4> + {testResults['RFQ 취소 (샘플)'] && ( + <TestResultCard result={testResults['RFQ 취소 (샘플)']} title="샘플 테스트" /> + )} + {testResults['RFQ 취소 (사용자)'] && ( + <TestResultCard result={testResults['RFQ 취소 (사용자)']} title="사용자 테스트" /> + )} + </div> + )} + </CardContent> + </Card> + </TabsContent> + + {/* RFQ 정보 탭 */} + <TabsContent value="rfq-info" className="space-y-6"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Send className="h-5 w-5" /> + RFQ 정보 전송 + </CardTitle> + <CardDescription> + RFQ 정보를 ECC로 전송합니다. (IF_EVCP_ECC_RFQ_INFORMATION) + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="space-y-4"> + <h4 className="font-semibold">RFQ 헤더 정보</h4> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Label htmlFor="rfq-anfnr">RFQ 번호 (필수)</Label> + <Input + id="rfq-anfnr" + value={rfqInfoData.ANFNR} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, ANFNR: e.target.value }))} + placeholder="RFQ 번호" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="rfq-lifnr">공급업체 계정 (필수)</Label> + <Input + id="rfq-lifnr" + value={rfqInfoData.LIFNR} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, LIFNR: e.target.value }))} + placeholder="공급업체 계정번호" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="rfq-waers">통화 (필수)</Label> + <Input + id="rfq-waers" + value={rfqInfoData.WAERS} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, WAERS: e.target.value }))} + placeholder="통화 코드 (예: KRW)" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="rfq-zterm">지불조건 (필수)</Label> + <Input + id="rfq-zterm" + value={rfqInfoData.ZTERM} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, ZTERM: e.target.value }))} + placeholder="지불조건 키" + /> + </div> + </div> + + <h4 className="font-semibold">RFQ 아이템 정보</h4> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Label htmlFor="rfq-anfps">아이템 번호 (필수)</Label> + <Input + id="rfq-anfps" + value={rfqInfoData.ANFPS} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, ANFPS: e.target.value }))} + placeholder="RFQ 아이템 번호" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="rfq-netpr">순가격 (필수)</Label> + <Input + id="rfq-netpr" + value={rfqInfoData.NETPR} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, NETPR: e.target.value }))} + placeholder="순가격" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="rfq-netwr">순주문가격 (필수)</Label> + <Input + id="rfq-netwr" + value={rfqInfoData.NETWR} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, NETWR: e.target.value }))} + placeholder="순주문가격" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="rfq-brtwr">총주문가격 (필수)</Label> + <Input + id="rfq-brtwr" + value={rfqInfoData.BRTWR} + onChange={(e) => setRfqInfoData(prev => ({ ...prev, BRTWR: e.target.value }))} + placeholder="총주문가격" + /> + </div> + </div> + </div> + + <Separator /> + + <div className="flex gap-4"> + <Button + onClick={() => runTest('RFQ 정보 (샘플)', () => sendTestRFQInformation())} + disabled={isLoading['RFQ 정보 (샘플)']} + variant="outline" + > + {isLoading['RFQ 정보 (샘플)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Play className="mr-2 h-4 w-4" /> + 샘플 데이터로 테스트 + </Button> + <Button + onClick={() => runTest('RFQ 정보 (사용자)', async () => { + const rfqRequest = { + T_RFQ_HEADER: [{ + ANFNR: rfqInfoData.ANFNR, + LIFNR: rfqInfoData.LIFNR, + WAERS: rfqInfoData.WAERS, + ZTERM: rfqInfoData.ZTERM, + INCO1: rfqInfoData.INCO1, + INCO2: rfqInfoData.INCO2, + MWSKZ: rfqInfoData.MWSKZ, + LANDS: rfqInfoData.LANDS, + VSTEL: rfqInfoData.VSTEL, + LSTEL: rfqInfoData.LSTEL + }], + T_RFQ_ITEM: [{ + ANFNR: rfqInfoData.ANFNR, + ANFPS: rfqInfoData.ANFPS, + NETPR: rfqInfoData.NETPR, + NETWR: rfqInfoData.NETWR, + BRTWR: rfqInfoData.BRTWR, + LFDAT: rfqInfoData.LFDAT + }] + } + return sendRFQInformation(rfqRequest) + })} + disabled={isLoading['RFQ 정보 (사용자)']} + > + {isLoading['RFQ 정보 (사용자)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Send className="mr-2 h-4 w-4" /> + 사용자 데이터로 전송 + </Button> + </div> + + {/* 테스트 결과 표시 */} + {(testResults['RFQ 정보 (샘플)'] || testResults['RFQ 정보 (사용자)']) && ( + <div className="space-y-2"> + <h4 className="font-semibold">테스트 결과</h4> + {testResults['RFQ 정보 (샘플)'] && ( + <TestResultCard result={testResults['RFQ 정보 (샘플)']} title="샘플 테스트" /> + )} + {testResults['RFQ 정보 (사용자)'] && ( + <TestResultCard result={testResults['RFQ 정보 (사용자)']} title="사용자 테스트" /> + )} + </div> + )} + </CardContent> + </Card> + </TabsContent> + + {/* PO 생성 탭 */} + <TabsContent value="po-create" className="space-y-6"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Send className="h-5 w-5" /> + PO (Purchase Order) 생성 + </CardTitle> + <CardDescription> + 구매주문 생성 요청을 ECC로 전송합니다. (IF_ECC_EVCP_PO_CREATE) + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="space-y-4"> + <h4 className="font-semibold">PO 헤더 정보</h4> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Label htmlFor="po-anfnr">입찰번호 (필수)</Label> + <Input + id="po-anfnr" + value={poData.ANFNR} + onChange={(e) => setPoData(prev => ({ ...prev, ANFNR: e.target.value }))} + placeholder="입찰번호" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="po-lifnr">공급업체 계정 (필수)</Label> + <Input + id="po-lifnr" + value={poData.LIFNR} + onChange={(e) => setPoData(prev => ({ ...prev, LIFNR: e.target.value }))} + placeholder="공급업체 계정번호" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="po-zproc">처리상태 (필수)</Label> + <Input + id="po-zproc" + value={poData.ZPROC_IND} + onChange={(e) => setPoData(prev => ({ ...prev, ZPROC_IND: e.target.value }))} + placeholder="구매처리상태" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="po-waers">통화 (필수)</Label> + <Input + id="po-waers" + value={poData.WAERS} + onChange={(e) => setPoData(prev => ({ ...prev, WAERS: e.target.value }))} + placeholder="통화 코드" + /> + </div> + </div> + + <h4 className="font-semibold">PO 아이템 정보</h4> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <Label htmlFor="po-anfps">입찰 아이템번호 (필수)</Label> + <Input + id="po-anfps" + value={poData.ANFPS} + onChange={(e) => setPoData(prev => ({ ...prev, ANFPS: e.target.value }))} + placeholder="입찰 아이템번호" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="po-netpr">순가격 (필수)</Label> + <Input + id="po-netpr" + value={poData.NETPR} + onChange={(e) => setPoData(prev => ({ ...prev, NETPR: e.target.value }))} + placeholder="순가격" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="po-bprme">주문단위 (필수)</Label> + <Input + id="po-bprme" + value={poData.BPRME} + onChange={(e) => setPoData(prev => ({ ...prev, BPRME: e.target.value }))} + placeholder="주문단위 (예: EA)" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="po-lfdat">납기일 (필수)</Label> + <Input + id="po-lfdat" + value={poData.LFDAT} + onChange={(e) => setPoData(prev => ({ ...prev, LFDAT: e.target.value }))} + placeholder="YYYYMMDD 형식" + /> + </div> + </div> + </div> + + <Separator /> + + <div className="flex gap-4"> + <Button + onClick={() => runTest('PO 생성 (샘플)', () => createTestPurchaseOrder())} + disabled={isLoading['PO 생성 (샘플)']} + variant="outline" + > + {isLoading['PO 생성 (샘플)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Play className="mr-2 h-4 w-4" /> + 샘플 데이터로 테스트 + </Button> + <Button + onClick={() => runTest('PO 생성 (사용자)', async () => { + const poRequest = { + T_Bidding_HEADER: [{ + ANFNR: poData.ANFNR, + LIFNR: poData.LIFNR, + ZPROC_IND: poData.ZPROC_IND, + ANGNR: poData.ANGNR, + WAERS: poData.WAERS, + ZTERM: poData.ZTERM, + INCO1: poData.INCO1, + INCO2: poData.INCO2, + MWSKZ: poData.MWSKZ, + LANDS: poData.LANDS, + ZRCV_DT: poData.ZRCV_DT, + ZATTEN_IND: poData.ZATTEN_IND, + IHRAN: poData.IHRAN, + TEXT: poData.TEXT + }], + T_Bidding_ITEM: [{ + ANFNR: poData.ANFNR, + ANFPS: poData.ANFPS, + LIFNR: poData.LIFNR, + NETPR: poData.NETPR, + PEINH: poData.PEINH, + BPRME: poData.BPRME, + NETWR: poData.NETWR, + BRTWR: poData.BRTWR, + LFDAT: poData.LFDAT + }], + T_PR_RETURN: [{ + ANFNR: poData.ANFNR, + ANFPS: poData.ANFPS, + EBELN: poData.EBELN, + EBELP: poData.EBELP, + MSGTY: poData.MSGTY, + MSGTXT: poData.MSGTXT + }] + } + return createPurchaseOrder(poRequest) + })} + disabled={isLoading['PO 생성 (사용자)']} + > + {isLoading['PO 생성 (사용자)'] && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Send className="mr-2 h-4 w-4" /> + 사용자 데이터로 생성 + </Button> + </div> + + {/* 테스트 결과 표시 */} + {(testResults['PO 생성 (샘플)'] || testResults['PO 생성 (사용자)']) && ( + <div className="space-y-2"> + <h4 className="font-semibold">테스트 결과</h4> + {testResults['PO 생성 (샘플)'] && ( + <TestResultCard result={testResults['PO 생성 (샘플)']} title="샘플 테스트" /> + )} + {testResults['PO 생성 (사용자)'] && ( + <TestResultCard result={testResults['PO 생성 (사용자)']} title="사용자 테스트" /> + )} + </div> + )} + </CardContent> + </Card> + </TabsContent> + </Tabs> + </div> + ) +} + +// 테스트 결과 표시 컴포넌트 +function TestResultCard({ result, title }: { result: TestResult; title: string }) { + return ( + <Alert className={result.success ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'}> + <div className="flex items-center gap-2"> + {result.success ? ( + <CheckCircle className="h-4 w-4 text-green-600" /> + ) : ( + <XCircle className="h-4 w-4 text-red-600" /> + )} + <span className="font-semibold">{title}</span> + <Badge variant="outline" className="text-xs"> + {result.duration}ms + </Badge> + <span className="text-xs text-muted-foreground ml-auto"> + {result.timestamp} + </span> + </div> + <AlertDescription className="mt-2"> + <div className="space-y-2"> + <p>{result.message}</p> + {result.responseData && ( + <details className="text-xs"> + <summary className="cursor-pointer font-medium">응답 데이터 보기</summary> + <pre className="mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto max-h-40"> + {result.responseData} + </pre> + </details> + )} + </div> + </AlertDescription> + </Alert> + ) +} |
